State Machine Pattern
↓これをstate machine pattenと呼ぶのかは不明だが、GPT-4.iconがそう言っていたので、そういうことにしておく
code:ts
type ShoppingCart =
| { kind: "empty" }
| { kind: "active", data: ActiveCartData }
| { kind: "paid", data: PaidCartData };
type ActiveCartData = { unpaidItems: string[] };
type PaidCartData = { paidItems: string[], payment: number };
code:ts
const addItem = (cart: ShoppingCart, item: string): ShoppingCart => {
switch(cart.kind) {
case "empty":
return { kind: "active", data: { unpaidItems: item }}; case "active":
case "paid":
return cart;
}
}
const makePayment = (cart: ShoppingCart, payment: number): ShoppingCart => {
switch(cart.kind) {
case "empty":
return cart;
case "active":
return { kind: "paid", data: { paidItems: cart.data.unpaidItems, payment }};
case "paid":
return cart;
}
}
stateに対し、actionが不適な場合の対応
code:ts
const addItem = (cart: ShoppingCart, item: string): ShoppingCart => {
switch(cart.kind) {
case "empty":
return { kind: "active", data: { unpaidItems: item }}; case "active":
case "paid":
return cart;
}
}
addItemというactionを、paidという状態に適用したとき、何もせず、cartをそのまま返している
型安全性と引数
こういうactionの型では、ShoppingCartをまるまる取るようにする
code:ts
const makePayment = (cart: ShoppingCart, payment: number): ShoppingCart => {
switch(cart.kind) {
case "empty":
return cart;
case "active":
return { kind: "paid", data: { paidItems: cart.data.unpaidItems, payment }};
case "paid":
return cart;
}
}
code:ts
type ActiveCart = Extract<Cart, {kind: 'active:}>
const makePayment = (cart: AcriveCart, payment: number): ShoppingCart => {
return { kind: "paid", data: { paidItems: cart.data.unpaidItems, payment }};
}
引数で、そもそも正しい状態でのみ実行できるようにしておく
こう述べられている
But if you did this, how would you handle the same event when the cart was in a different state, such as empty or paid? Someone has to handle the event for all three possible states somewhere, and it is much better to encapsulate this business logic inside the function than to be at the mercy of the caller.
しかし、このようにした場合、カートが空や支払い済みなどの異なる状態にあるときに、同じイベントをどのように処理するのでしょうか?そして、このビジネスロジックを関数の中にカプセル化することは、呼び出し元の言いなりになるよりもずっと良いことです。DeepL.icon
ここでは内部でhandlingした方が良い、という判断をしている
その理由が、カプセル化
ShoppingCartという状態を扱う際に、「各状態ごとに何をする」というロジックをShoppingCartを定義したmodule内に閉じ込めることができている
外部でhandlingするようにすると、利用者が状態ごとに何をするか、というのを逐一判断する必要が出てくる
ダルいけど柔軟とも言える
これだめなのかなあ
抽象度を揃えて書くでは、高いレイヤーではパターンマッチだけになる、みたいに書いたけど、これも見直しても良いかもしれないmrsekut.icon どこでhandlingするか、の観点を交えて
各状態を外側に表出させない(=カプセル化する)と判断したならそうするしかないが、表出させることが適する場面もあると思う
PaidCartDataのみを引数に取る関数を用意する
payment reportを発行する関数は、PaidCartDataのみが対象になるので正しい
この場合は、このpaymentReport関数の呼び出し時に、状態のhandlingが必要
あと、TypeScriptは引数の型で返り値の型の条件分岐ができるので、より柔軟な対応は取れる
やるかどうかはさておき
F#でもoverloadとかはあるだろうけど、今回の対応は無理だろう
なので
every branch of the match must return the same type, so when ignoring the verified state we must still return something, such as the object that was passed in.
になる
どこでハンドリングするか?
カプセル化すべきかどうか?
不適な状態でactionを呼んだ時に、「何も起こらない」という挙動で良いのかどうか?
全く同じ見た目のボタンを連打した時に発火するような使われ方を想定するならそれで良さそう
でも、状態のごとに呼び出し方が異なるような状況を想定するなら、引数を型安全にした方がカタい